﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Shared.Pools;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux;

/// <remarks>
/// We are reading files from /proc and /cgroup. Those files are dynamically generated by kernel, when the access to them is requested.
/// Those files then, are stored entirely in RAM; it is called Virtual File System (VFS). The access to the files is done by syscall that is non-blocking.
/// Thus, this API can be synchronous without performance loss.
/// </remarks>
internal sealed class OSFileSystem : IFileSystem
{
    public bool Exists(FileInfo fileInfo)
    {
        return fileInfo.Exists;
    }

    public IReadOnlyCollection<string> GetDirectoryNames(string directory, string pattern)
    {
        return Directory.GetDirectories(directory, pattern)
                .ToArray();
    }

    public int Read(FileInfo file, int length, Span<char> destination)
    {
        using var stream = file.OpenRead();
        using var rentedBuffer = new RentedSpan<byte>(length);

        var read = stream.Read(rentedBuffer.Span);

        return Encoding.ASCII.GetChars(rentedBuffer.Span.Slice(0, read), destination);
    }

    public void ReadFirstLine(FileInfo file, BufferWriter<char> destination)
        => ReadUntilTerminatorOrEnd(file, destination, (byte)'\n');

    public void ReadAll(FileInfo file, BufferWriter<char> destination)
        => ReadUntilTerminatorOrEnd(file, destination, null);

    public IEnumerable<ReadOnlyMemory<char>> ReadAllByLines(FileInfo file, BufferWriter<char> destination)
    {
        const int MaxStackalloc = 256;

        if (!file.Exists)
        {
            throw new FileNotFoundException();
        }

        Memory<byte> buffer = ArrayPool<byte>.Shared.Rent(MaxStackalloc);
        try
        {
            using FileStream stream = file.OpenRead();

            int read;
            while ((read = stream.Read(buffer.Span)) > 0)
            {
                var start = 0;
                for (int end = 0; end < read; end++)
                {
                    if (buffer.Span[end] == (byte)'\n')
                    {
                        var length = end - start;

                        // Decode the slice into chars
                        _ = Encoding.ASCII.GetChars(buffer.Span.Slice(start, length), destination.GetSpan(length));
                        destination.Advance(length);
                        start = end + 1;

                        // Yield the completed line
                        yield return destination.WrittenMemory;
                        destination.Reset();
                    }
                }

                // Set the comparison in the while loop to end when the file has not been completely read into the buffer.
                // It will then advance the last character to the destination for the next time yield return is called.
                if (start < read)
                {
                    var length = read - start;
                    _ = Encoding.ASCII.GetChars(buffer.Span.Slice(start, length), destination.GetSpan(length));
                    destination.Advance(length);
                }
            }

            // After reaching EOF, check for any leftover characters
            if (destination.WrittenMemory.Length > 0)
            {
                // Yield the last line (without a trailing newline)
                yield return destination.WrittenMemory;
                destination.Reset();
            }
        }
        finally
        {
            // Return the rented buffer to the pool
            if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> arraySegment) && arraySegment.Array != null)
            {
                ArrayPool<byte>.Shared.Return(arraySegment.Array);
            }
        }
    }

    [SkipLocalsInit]
    private static void ReadUntilTerminatorOrEnd(FileInfo file, BufferWriter<char> destination, byte? terminator)
    {
        const int MaxStackalloc = 256;

        using var stream = file.OpenRead();

        Span<byte> buffer = stackalloc byte[MaxStackalloc];
        var read = stream.Read(buffer);

        while (read != 0)
        {
            var end = 0;

            for (end = 0; end < read; end++)
            {
                if (buffer[end] == terminator)
                {
                    _ = Encoding.ASCII.GetChars(buffer.Slice(0, end), destination.GetSpan(end));
                    destination.Advance(end);

                    return;
                }
            }

            _ = Encoding.ASCII.GetChars(buffer.Slice(0, end), destination.GetSpan(end));
            destination.Advance(end);
            read = stream.Read(buffer);
        }
    }
}
